import React, { useState, useEffect } from 'react'
import { Navigate, useSearchParams, Link } from 'react-router-dom'
import { Field, Form, Formik } from 'formik'
import { trpc, client, inferMutationInput } from '~/utils/trpc'
import IVInputField from '~/components/IVInputField'
import IVButton from '~/components/IVButton'
import { Organization } from '@prisma/client'
import { notify } from '~/components/NotificationCenter'
import { isOrgSlugValid } from '~/utils/validate'
import useDashboard, { useHasPermission } from '~/components/DashboardContext'
import Dialog, { useDialogState } from '~/components/IVDialog'
import IVTextInput from '~/components/IVTextInput'
import IVTooltip from '~/components/IVTooltip'
import IVCheckbox from '~/components/IVCheckbox'
import SlackIcon from '~/icons/compiled/Slack'
import PageHeading from '~/components/PageHeading'
import IVSpinner from '~/components/IVSpinner'
import NavTabs, { NavTab } from '~/components/NavTabs'
import classNames from 'classnames'
function TabSubSection({
title,
children,
className,
}: {
title: string
className?: string
children: React.ReactNode
}) {
return (
)
}
export const ORG_SLUG_CONSTRAINTS =
'Must be at least 2 characters and can only contain lowercase letters, numbers, hyphens, and underscores.'
export async function validateOrgSlugChange(id: string, slug: string) {
if (!isOrgSlugValid(slug)) {
return 'Slugs must be at least 2 characters and can only contain lowercase letters, numbers, hyphens, and underscores.'
}
const canChange = await client.query('organization.is-slug-available', {
slug,
id,
})
if (!canChange) {
return 'Sorry, this slug is taken. Please select another.'
}
}
function EditOrganizationForm({ org }: { org: Organization }) {
const ctx = trpc.useContext()
const editOrganization = trpc.useMutation('organization.edit')
return (
['data']>
initialValues={{
slug: org.slug,
name: org.name,
}}
initialTouched={{
// We want any errors to display immediately on change, not on blur
slug: true,
}}
onSubmit={async data => {
if (editOrganization.isLoading) return
editOrganization.mutate(
{
id: org.id,
data,
},
{
onSuccess(newOrg) {
if (newOrg.slug === org.slug) {
notify.success('Your changes were saved.')
ctx.refetchQueries(['organization.slug'])
} else {
window.location.assign(
`/dashboard/${newOrg.slug}/organization/settings?nc-save-success`
)
}
},
}
)
}}
>
{({ errors, touched }) => (
)}
)
}
function OrganizationMFAForm() {
const editMfa = trpc.useMutation('organization.edit.mfa')
const session = trpc.useQuery(['auth.session.session'])
const { organization, integrations } = useDashboard()
const ctx = trpc.useContext()
const canEnable = !!integrations?.workos && session.data?.hasMfa
return (
>
initialValues={{
requireMfa: organization.requireMfa,
}}
onSubmit={async data => {
if (!canEnable || editMfa.isLoading) return
editMfa.mutate(data, {
onSuccess(newOrg) {
notify.success('Your changes were saved.')
ctx.setQueryData(['organization.slug', { slug: newOrg.slug }], {
...organization,
requireMfa: newOrg.requireMfa,
})
},
})
}}
>
{({ values, setFieldValue }) => (
)}
)
}
function ArchiveForm() {
const { organization, me } = useDashboard()
const dialog = useDialogState()
const archive = trpc.useMutation('organization.delete')
const [slugConfirmation, setSlugConfirmation] = useState('')
const { visible, animating } = dialog
useEffect(() => {
if (!visible && !animating) {
setSlugConfirmation('')
}
}, [visible, animating])
const hasOtherOrganizations =
me.userOrganizationAccess.filter(
access => access.organization.ownerId === me.id
).length > 1
return (
{hasOtherOrganizations ? (
) : (
)}
)
}
function SlackIntegrationsForm() {
const startOauth = trpc.useMutation('organization.start-slack-oauth')
const { organization } = useDashboard()
const onClick = e => {
e.preventDefault()
if (startOauth.isLoading) return
startOauth.mutate(null, {
onSuccess(authUrl) {
window.location.href = authUrl
},
})
}
const [searchParams, setSearchParams] = useSearchParams()
const oauthMessage = code => {
switch (code) {
case 'success':
return 'Connected your Slack workspace'
case 'access_denied':
return 'Must allow permissions to connect to Slack'
case 'invalid_state_param':
return 'Invalid OAuth state, try again'
default:
return 'Something went wrong'
}
}
useEffect(() => {
// check for some query param in redirect from
// web/src/server/api/auth/oauth/slack.ts
const oauthResult = searchParams.get('oauth_result')
if (oauthResult) {
const toastDuration = 4000
if (oauthResult === 'success' && organization.connectedToSlack) {
notify.success(oauthMessage(oauthResult), {
id: 'oauth-toast',
duration: toastDuration,
})
} else {
// if 'success' but still not connected, override success message
const result =
oauthResult === 'success' ? 'Something went wrong' : oauthResult
notify.error(oauthMessage(result), {
id: 'oauth-toast',
duration: toastDuration,
})
}
const errorTimeout = setTimeout(() => setSearchParams({}), toastDuration)
return () => clearTimeout(errorTimeout)
}
})
return (
{organization.connectedToSlack ? (
<>
Your Slack workspace is connected to Interval 👍
Click here to reconnect
.
To send notifications to a Slack channel
, you'll also have to add the Interval app to that channel.
>
) : (
<>
Connect your Slack workspace to Interval to enable{' '}
sending notifications to Slack channels or users
.
{startOauth.error && (
{startOauth.error.message}
)}
Add to Slack
}
onClick={onClick}
/>
>
)}
)
}
export default function OrganizationSettings() {
const { organization, me, integrations } = useDashboard()
const canConnectOauth = useHasPermission('WRITE_ORG_OAUTH')
const canWriteSettings = useHasPermission('WRITE_ORG_SETTINGS')
const [searchParams] = useSearchParams()
const tab = searchParams.get('tab')
if (canWriteSettings === undefined) {
return
}
if (canWriteSettings === false) {
return
}
let canAccessIntegrations = canConnectOauth && integrations?.slack
const navItems: NavTab[] = [{ path: '', tab: null, label: 'General' }]
if (integrations?.workos) {
navItems.push({
path: '?tab=security',
tab: 'security',
label: 'Security',
})
}
if (canAccessIntegrations) {
navItems.push({
path: '?tab=integrations',
tab: 'integrations',
label: 'Integrations',
enabled: canConnectOauth,
})
}
return (
({
...item,
path: `/dashboard/${organization.slug}/organization/settings${item.path}`,
}))}
/>
{tab === 'security' && integrations?.workos ? (
) : tab === 'integrations' && canAccessIntegrations ? (
) : (
<>
{me.id === organization.ownerId &&
}
>
)}
)
}